feat: add profile-based routing foundation#161
Conversation
|
I'll review this in the morning! Thank you for. the submission and ideas! |
rynfar
left a comment
There was a problem hiding this comment.
Review
Good work on this — the scope is right, the session isolation approach is clean, and the per-profile auth caching is well done. I want to merge this once two things are adjusted:
1. Move getProfileId out of AgentAdapter
x-meridian-profile is Meridian proxy infrastructure, not an agent-specific concern. Every adapter would implement it identically — it's just c.req.header("x-meridian-profile"). The AgentAdapter interface should stay focused on abstracting agent differences (OpenCode vs. future agents).
Read the header directly in server.ts instead:
const requestedProfileId = c.req.header("x-meridian-profile")
const requestedProfile = resolveProfile(finalConfig, requestedProfileId)And drop getProfileId from adapter.ts and adapters/opencode.ts.
2. Handle unknown profile as a 400, not a 500
Right now if a client sends x-meridian-profile: nonexistent, resolveProfile() throws, which hits the generic catch in server.ts and returns a 500 with a stack trace. This should be a clean 400.
Easiest fix — catch it at the call site in server.ts:
let requestedProfile: ResolvedProfile
try {
requestedProfile = resolveProfile(finalConfig, requestedProfileId)
} catch (e) {
return c.json({
type: "error",
error: { type: "invalid_request_error", message: e instanceof Error ? e.message : "Invalid profile" },
}, 400)
}Everything else looks good. The buildScopedKey approach, the per-profile auth status cache, and the test coverage are all solid. Happy to merge once these two are in.
|
Thanks for the feedback. Whenever I get some free time I'll get the suggestions added in, and continue testing. |
|
This is a genuinely useful feature and I spent some time redesigning this to make it more meaningful. I've added support for multiple profiles. There is also a UI to help you manage your profiles with clear instructions in how to authenticate each profile and how to switch bertween them easily - there is also a cli for this if you prefer the CLI. Right now you can only have 1 profile active at a time, i may extend this to allow 2 profiles under different ports running at the same time depending on if this is a mode people want. Right now you will be able to create propfiles and simply swap between them at the proxy level, this will allow you to swap without losing resumability for a session you currentlyl have active. Give it a spin it will be in the next release. I am doing some testing and then i will release it. |
|
I haven't had time, but I'll give it a whirl. One thing I should mention is the refresh token wasn't working on the secondary Claude/non-default path. Let me know if you run into the same issue. It may have been a docker issue or something I had misconfigured. |
Yea I noticed that while i was testing and am fixing this as part of it. I should have a release for this later today once I've had enough time to stress test it a bit. The UI will keep track of the last successful refresh for each profile and will refresh both profiles. You still have to manually log out of oauth before adding a second account. This part is janky and really hard to code around so its not really worth it. For now you just have to manage the login yourself. But once logged in the proxy will keep your profiles refreshed. I put clear instructions in the UI so its easy to follow. |
Run one Meridian instance with multiple Claude accounts. Each profile
is a named auth context with its own CLAUDE_CONFIG_DIR and OAuth tokens.
Unlimited profiles supported.
CLI commands:
meridian profile add <name> # authenticate and add a profile
meridian profile list # show profiles and auth status
meridian profile switch <name> # switch active profile on running proxy
meridian profile remove <name> # remove a profile
meridian profile login <name> # re-authenticate a profile
Profile selection priority:
1. x-meridian-profile request header (per-request override)
2. Active profile (set via UI dropdown, CLI, or POST /profiles/active)
3. Default profile from config
4. First configured profile
UI:
- Global sticky profile bar on all pages (landing + telemetry)
- Profile dropdown with instant switching
- Nav links (Home / Telemetry)
- Auto-hides when no profiles configured
API:
- GET /profiles — list profiles with active indicator
- POST /profiles/active — switch the active profile
Internals:
- Profiles stored in ~/.config/meridian/profiles.json
- Per-profile auth dirs in ~/.config/meridian/profiles/{id}/
- Auto-loads from profiles.json when MERIDIAN_PROFILES env not set
- Per-profile auth status caching (keyed by env overrides)
- Session resume scoped by profile ID
- Background auth keepalive for all profiles (45s interval)
- Zero impact when no profiles configured
Addresses #161
…ng (#279) * feat: multi-profile support — switch Claude accounts without restarting Run one Meridian instance with multiple Claude accounts. Each profile is a named auth context with its own CLAUDE_CONFIG_DIR and OAuth tokens. Unlimited profiles supported. CLI commands: meridian profile add <name> # authenticate and add a profile meridian profile list # show profiles and auth status meridian profile switch <name> # switch active profile on running proxy meridian profile remove <name> # remove a profile meridian profile login <name> # re-authenticate a profile Profile selection priority: 1. x-meridian-profile request header (per-request override) 2. Active profile (set via UI dropdown, CLI, or POST /profiles/active) 3. Default profile from config 4. First configured profile UI: - Global sticky profile bar on all pages (landing + telemetry) - Profile dropdown with instant switching - Nav links (Home / Telemetry) - Auto-hides when no profiles configured API: - GET /profiles — list profiles with active indicator - POST /profiles/active — switch the active profile Internals: - Profiles stored in ~/.config/meridian/profiles.json - Per-profile auth dirs in ~/.config/meridian/profiles/{id}/ - Auto-loads from profiles.json when MERIDIAN_PROFILES env not set - Per-profile auth status caching (keyed by env overrides) - Session resume scoped by profile ID - Background auth keepalive for all profiles (45s interval) - Zero impact when no profiles configured Addresses #161 * fix: address code review findings for multi-profile feature P0: Set 0o600 permissions on profiles.json (API keys not world-readable) P1: Replace empty catch blocks with console.warn logging P1: Add 21 unit tests for profiles.ts pure functions P2: Import ProfileConfig from profiles.ts (eliminate 3 duplicate types) P2: Use profile ID as auth cache key instead of JSON.stringify(envOverrides) P2: Cache loadProfilesFromDisk() with 5s TTL (avoid sync I/O per request) P2: HTML-escape profile IDs in profilePage.ts and profileBar.ts (XSS fix) P2: Add try/catch for JSON parsing in POST /profiles/active P2: Add CLAUDE_PROXY_PORT/HOST fallback in profileSwitch() P2: Update ARCHITECTURE.md module map and dependency rules P2: Update CLAUDE.md stable API contract with new profile endpoints/headers P3: Add multi-profile documentation to README with examples
Summary
x-meridian-profileUse Case
I have both a personal and a company Claude account/plan, and I want to use OpenCode from several systems without having to install and auth Meridian separately on each machine.
The goal is to run a single Meridian instance that I can reach over my local network or a private Tailscale tunnel, then route requests to the right Claude profile from each client. That gives me better control over which systems use which account, while still feeling similar to how each machine might otherwise have its own
claude authsetup.This PR only adds the profile-routing foundation. It does not add request authentication or admin-route protection yet.
Testing
bun test src/__tests__/proxy-profiles.test.tsbun test src/__tests__/proxy-stale-uuid-retry.test.tsnpm run buildNotes
Follow-up PRs will keep the maintainer review small:
/healthand/telemetry*